/*
 * Process -- manage i/o with simple subprocesses.
 * Considerably tweaked relative of /NextDeveloper/Examples/Subprocess.
 * M. J. Hawley
 * Copyright (c) MIT Media Laboratory
 * mike@media-lab.mit.edu
 */
#import "Process.h"
#import <appkit/nextstd.h>
#import <appkit/Application.h>
#import <appkit/Panel.h>

@interface Process(Private)
- childDidExit;
- fdHandler:(int)theFd;
@end

static void
showError (const char *s, id delegate){ // ensure errors never get lost
    if (delegate && [delegate respondsTo:@selector(processError:)])
	[delegate perform:@selector(processError:) with:(void *)s];
    else if (NXApp)	// no delegate, but we're running w/in an App
	NXRunAlertPanel(0, s, 0, 0, 0);
    else
	perror(s);
}

static void
fdHandler (int fd, id self) { // DPS handler for output from process
    [self fdHandler:fd];
}

static void
getptys(int *master, int *slave){ // attempt to setup the ptys
    #define	PTY_TEMPLATE "/dev/pty??"
    #define	PTY_LENGTH 11
    char device[PTY_LENGTH];
    char *block, *num;
    char *blockLoc; // specifies the location of block for the device string
    char *numLoc; // specifies the pty name with the digit ptyxD
    char *msLoc; // specifies the master (ptyxx) or slave (ttyxx)
    
    struct sgttyb setp = {B9600, B9600, (char)0x7f, (char)0x15, (CRMOD|ANYP)};
    struct tchars setc = {CINTR, CQUIT, CSTART, CSTOP, CEOF, CBRK};
    struct ltchars sltc = {CSUSP, CDSUSP, CRPRNT, CFLUSH, CWERASE, CLNEXT};
    int	lset = (LCRTBS|LCRTERA|LCRTKIL|LCTLECH|LPENDIN|LDECCTQ);
    int	setd = NTTYDISC;
    
    strcpy(device, PTY_TEMPLATE); // string constants are not writable
    blockLoc = &device[ strlen("/dev/pty") ];
    numLoc = &device[ strlen("/dev/pty?") ];
    msLoc = &device[ strlen("/dev/") ];
    for (block = "pqrs"; *block; block++){
	*blockLoc = *block;
	for (num = "0123456789abcdef"; *num; num++) {
	    *numLoc = *num;
	    *master = open(device, O_RDWR);
	    if (*master >= 0) {
		*msLoc = 't';
		*slave = open(device, O_RDWR);
		if (*slave >= 0) {
		    (void) ioctl(*slave, TIOCSETP, (char *)&setp);
		    (void) ioctl(*slave, TIOCSETC, (char *)&setc);
		    (void) ioctl(*slave, TIOCSETD, (char *)&setd);
		    (void) ioctl(*slave, TIOCSLTC, (char *)&sltc);
		    (void) ioctl(*slave, TIOCLSET, (char *)&lset);
		    return;
		}
	    }
	} /* hunting through a bank of ptys */
    } /* hunting through blocks of ptys in all the right places */
    *master = -1;
    *slave = -1;
}

static int
iwait(fd, timeout)
        long fd;
        unsigned long timeout;  /* in seconds */
/*
 * Wait until 'fd' is ready for reading, or 'timeout'.
 * Return '>=0' when 'fd' is readable, '0' if timeout, '-1' on error.
 * Example: 'iwait(f,0)' polls a file descriptor
 * without blocking and returns true if it's readable;
 * e.g., 'iwait(0,0)' is true when standard input is contains something.
 */
{
        struct timeval t;
        int readfd = 1<<fd;

        t.tv_sec = timeout, t.tv_usec = 0;
        return (int)select(sizeof(int)*8,  &readfd, (int *)0, (int *)0, &t);
}

@implementation Process(Private)

- childDidExit { // cleanup after a child process exits
    if (childPid) {
	if (from) DPSRemoveFD(from);
	close(from);
	if (fpTo) fclose(fpTo);
	if (fpFrom) fclose(fpFrom);
	fpTo = fpFrom = (FILE *)0;
	childPid=0;	// specify that child is dead
	if (delegate && [delegate respondsTo:@selector(processDone)])
	    [delegate perform:@selector(processDone)];
    }
    return self;
}

- fdHandler:(int)fd { // DPS handler for output from process
    if (iwait(fd,1)<=0) return self;
    if (((bufferCount=read(fd,buffer,BUFSIZE-1))<0)||(!bufferCount))
	return [self childDidExit];
    buffer[bufferCount] = '\0';
    if (delegate && [delegate respondsTo:action])
	[delegate perform:action with:(void *)&buffer];
    return self;
}

@end


@implementation Process

+ new:(char *)process delegate:del {
    self = [Process alloc];
    [self init:process delegate:del andPty:YES andStderr:YES];
    return self;
}

+ new:(char *)process delegate:del andPty:(BOOL)wantsPty andStderr:(BOOL)wantsStderr {
    self = [Process alloc];
    [self init:process delegate:del andPty:wantsPty andStderr:wantsStderr];
    return self;
}

- init:(char *)process delegate:del {
    return [self init:process delegate:del andPty:NO andStderr:NO];
}

- init:(char *)process delegate:del andPty:(BOOL)pty andStderr:(BOOL)err {
    // initializes an instance of process and corresponding UNIX process
    int pipeTo[2];
    int pipeFrom[2];
    int	tty, numFds, fd;	// for temporary use
    int processGroup;
    int pidChild;		// needed because childPid does not exist
				// until process is instantiated

    [self setAction:@selector(processOutput:)];
    if (pty){
    	tty = open("/dev/tty", O_RDWR);
	getptys(&masterPty,&slavePty);
	if (masterPty <= 0 || slavePty <= 0) {
	    showError("Error grabbing ptys for subprocess.", del);
	    return self;
	}
	// remove the controlling tty if launched from a shell,
	// but not Workspace;
	// so that we have job control over the parent application in shell
	// and so that subprocesses can be restarted in Workspace
	if  ((tty<0) && ((tty = open("/dev/tty", 2))>=0)) {
	    ioctl(tty, TIOCNOTTY, 0);
	    close(tty);
	}
    } else
    if (pipe(pipeTo) < 0 || pipe(pipeFrom) < 0){
	showError("Error starting UNIX pipes to process.", del);
	return self;
    }
    
    switch (pidChild = vfork()){
    case -1:	// error
	showError("Error starting UNIX vfork of process.", del);
	return self;

    case 0:	// child
	if (pty) {
	    dup2(slavePty, 0);
	    dup2(slavePty, 1);
	    if (err) dup2(slavePty, 2);
	} else {
	    dup2(pipeTo[0], 0);
	    dup2(pipeFrom[1], 1);
	    if (err) dup2(pipeFrom[1], 2);
	}
	
	numFds = getdtablesize();
	for (fd=3; fd<numFds; fd++)
	    close(fd);

	processGroup = getpid();
	ioctl(0, TIOCSPGRP, (char *)&processGroup);
	setpgrp (0, processGroup);
	
	// execl(process, 0);
	execl("/bin/sh", "sh", "-c", process, 0);
	perror("vfork (child)"); // should never gets here tho
	exit(1);

    default:	// parent
	[self setDelegate:del];
	childPid = pidChild;
	if (pty){
	    close(slavePty);
	    fpTo = fdopen(masterPty, "w");
	    from = masterPty;
	    fpFrom = fdopen(masterPty, "r");
	} else {
	    close(pipeTo[0]);
	    close(pipeFrom[1]);
    
	    fpTo = fdopen(pipeTo[1], "w");
	    from = pipeFrom[0];
	    fpFrom = fdopen(pipeFrom[0], "r");
        }
	setbuf(fpTo, NULL);
	setbuf(fpFrom, NULL);
	DPSAddFD(from,(DPSFDProc)fdHandler,(id)self,NX_MODALRESPTHRESHOLD+1);
	// printf("added %d, %d, %d [%d %d]\n",from, self, delegate, pipeTo[1], pipeFrom[0]);
	return self;
    }
}

- puts:(char *)s {
    fputs(s, fpTo);
    return self;
}

#define DELAY 15

static void
_fgets(s,n,f) char *s; int n; FILE *f; {
    int fd = fileno(f);
    while (iwait(fd, DELAY)>0){
        if (read(fd,s,1) != 1){
            *s = '\0';
            return;
        }
        if (*s == 27) *s = '\n';
        if (*s=='\n' || --n <= 0){
            *++s = '\0';
            return ;
        }
        if (*s != '\r') ++s;
    }
    *++s = '\0';
}

- gets:(char *)s :(int)n {
    *s = '\0';
    _fgets(s,n,fpFrom);
//    fgets(s,n,fpFrom);
    return self;
}

- (char) getc {
    char c;
    int fd = fileno(fpFrom);
    iwait(fd, DELAY);
    return (read(fd,&c,1)==1)? c : '\0';
}

- terminate:sender{
    if (childPid){
	kill(childPid+1, SIGTERM);
	[self childDidExit];
    }
    return self;
}

- setDelegate:anObject {
    delegate = anObject;
    return self;
}

- delegate {
    return delegate;
}

- setAction:(SEL)theAction {
    action = theAction;
    return self;
}
@end
